package org.greyhawk.logger;

import org.slf4j.Marker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IllegalFormatException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

/**
 * Created at 6:21 PM on 8/20/12
 *
 * @author Russ Jackson (last updated by $Author$)
 * @version $Revision$, $Date$
 */
public class Logger {

    public static enum LogLevel {
        debug, trace, info, warn, error
    }

    private Queue<LogMessage> _messageQueue = new LinkedList<LogMessage>();

    private static boolean _globalMethodReporting = true;

    /**
     * turns on [class.method():line] entries in the log strings globally
     * across all loggers
     */
    public static void enableGlobalMethodReporting() { _globalMethodReporting = true; }

    /**
     * turns off [class.method():line] entries in the log strings globally across
     * all loggers
     */
    public static void disableGlobalMethodReporting() { _globalMethodReporting = false; }

    private boolean _methodReporting = true;

    /**
     * turns on [class.method():line] entries in the log strings for this
     * instance only
     */
    public void enableMethodReporting() { _methodReporting = true; }

    /**
     * turns off [class.method():line] entries in the log strings for this
     * instance only
     */
    public void disableMethodReporting() { _methodReporting = false; }

    private boolean wantMethodAndLine() {
        return _globalMethodReporting && _methodReporting;
    } // wantMethodAndLine()

    private org.slf4j.Logger _wrappedLogger;

    org.slf4j.Logger getWrappedLogger() {
        return _wrappedLogger;
    } // getWrappedLogger()

    /**
     * constructs a new Logger instance as a wrapper around the provided
     * slf4j Logger instance
     * @param wrappedLogger the slf4j Logger instance that will be wrapped
     *                      inside this logger
     */
    public Logger(org.slf4j.Logger wrappedLogger) {
        if (null == wrappedLogger) {
            throw new IllegalArgumentException("logger is required");
        }
        _wrappedLogger = wrappedLogger;
        start();
    } // constructor

    synchronized private void pushMessage(LogMessage message) {
        _messageQueue.add(message);
    } // pushMessage()

    synchronized private LogMessage popMessage() {
        return _messageQueue.poll();
    } // popMessage()

    private int _throttle = 0;
    private void throttle() throws InterruptedException {
        if (_throttle < 100) {
            _throttle += 10;
        }
        Thread.sleep(_throttle);
    } // throttle()

    private void resetThrottle() {
        _throttle = 0;
    } // resetThrottle()

    private Map<String, Method> _methodMap = new HashMap<String, Method>();

    private Method getMethod(String key, String name, Class... params)
        throws NoSuchMethodException {
        String methodKey = String.format("%s.%s", key, name);
        Method method = _methodMap.get(methodKey);
        if (null == method) {
            method = _wrappedLogger.getClass().getMethod(name, params);
            _methodMap.put(methodKey, method);
        }
        return method;
    } // getMethod()

    private void processMessage() throws InterruptedException {
        LogMessage logMessage = popMessage();
        if (null == logMessage) {
            throttle();
        } else {
            resetThrottle();

            String message = null;
            if (logMessage.hasArgs()) {
                message = getMessage(
                    logMessage.getTrace(),
                    logMessage.getMessage(),
                    logMessage.getArgs()
                );
            } else {
                message = getMessage(
                    logMessage.getTrace(),
                    logMessage.getMessage()
                );
            }

            try {
                if (logMessage.hasMarker()) {
                    Method method = getMethod(
                        "key.marker",
                        logMessage.getLogLevel().name(),
                        Marker.class,
                        String.class
                    );
                    method.invoke(
                        _wrappedLogger,
                        logMessage.getMarker(),
                        message
                    );
                } else if (logMessage.hasThrowable()) {
                    Method method = getMethod(
                        "key.throwable",
                        logMessage.getLogLevel().name(),
                        String.class,
                        Throwable.class
                    );
                    method.invoke(
                        _wrappedLogger,
                        message,
                        logMessage.getThrowable()
                    );
                } else {
                    Method method = getMethod(
                        "key.plain",
                        logMessage.getLogLevel().name(),
                        String.class
                    );
                    method.invoke(
                        _wrappedLogger,
                        message
                    );
                }
            } catch (NoSuchMethodException nsme) {
                nsme.printStackTrace();
            } catch (InvocationTargetException ite) {
                ite.printStackTrace();
            } catch (IllegalAccessException iae) {
                iae.printStackTrace();
            }
        }
    } // processMessage()

    private void start() {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    try {
                        processMessage();
                    } catch (InterruptedException e) {
                        e.printStackTrace(); // @todo
                    }
                }
            }
        });
        thread.start();
    } // start()

    /*----------------------------------------------------------------------------
        debug level logging statements
    -----------------------------------------------------------------------------*/

    public void debug(String string) {
        if (isDebugEnabled()) {
            postLogMessage(LogLevel.debug, null, string);
        }
    } // debug()

    public void debug(Marker marker, String string) {
        if (isDebugEnabled(marker)) {
            postLogMessage(LogLevel.debug, marker, string);
        }
    } // debug()

    public void debug(String formatString, Object... args) {
        if (isDebugEnabled()) {
            postLogMessage(LogLevel.debug, null, formatString, args);
        }
    } // debug()

    public void debug(Marker marker, String formatString, Object... args) {
        if (isDebugEnabled(marker)) {
            postLogMessage(LogLevel.debug, null, formatString, args);
        }
    } // debug()

    public void debug(String string, Throwable throwable) {
        if (isDebugEnabled()) {
            postLogMessage(LogLevel.debug, string, throwable);
        }
    } // debug()

    public void debug(Throwable throwable, String string, Object... args) {
        if (isDebugEnabled()) {
            postLogMessage(LogLevel.debug, string, throwable, args);
        }
    } // debug()

    public boolean isDebugEnabled(Marker marker) {
        return _wrappedLogger.isDebugEnabled(marker);
    } // isDebugEnabled()

    public boolean isDebugEnabled() {
        return _wrappedLogger.isDebugEnabled();
    } // isDebugEnabled()

    /*----------------------------------------------------------------------------
        trace level logging statements
    -----------------------------------------------------------------------------*/

    public void trace(String string) {
        if (isTraceEnabled()) {
            postLogMessage(LogLevel.trace, null, string);
        }
    } // trace()

    public void trace(Marker marker, String string) {
        if (isTraceEnabled(marker)) {
            postLogMessage(LogLevel.trace, marker, string);
        }
    } // trace()

    public void trace(String formatString, Object... args) {
        if (isTraceEnabled()) {
            postLogMessage(LogLevel.trace, null, formatString, args);
        }
    } // trace()

    public void trace(Marker marker, String formatString, Object... args) {
        if (isTraceEnabled(marker)) {
            postLogMessage(LogLevel.trace, marker, formatString, args);
        }
    } // trace()

    public void trace(String string, Throwable throwable) {
        if (isTraceEnabled()) {
            postLogMessage(LogLevel.trace, string, throwable);
        }
    } // trace()

    public void trace(Throwable throwable, String string, Object... args) {
        if (isTraceEnabled()) {
            postLogMessage(LogLevel.trace, string, throwable, args);
        }
    } // trace()

    public boolean isTraceEnabled(Marker marker) {
        return _wrappedLogger.isTraceEnabled(marker);
    } // isTraceEnabled()

    public boolean isTraceEnabled() {
        return _wrappedLogger.isTraceEnabled();
    } // isTraceEnabled()

    /*----------------------------------------------------------------------------
        info level logging statements
    -----------------------------------------------------------------------------*/

    public void info(String string) {
        if (isInfoEnabled()) {
            postLogMessage(LogLevel.info, null, string);
        }
    } // info()

    public void info(Marker marker, String string) {
        if (isInfoEnabled(marker)) {
            postLogMessage(LogLevel.info, marker, string);
        }
    } // info()

    public void info(String formatString, Object... args) {
        if (isInfoEnabled()) {
            postLogMessage(LogLevel.info, null, formatString, args);
        }
    } // info()

    public void info(Marker marker, String formatString, Object... args) {
        if (isInfoEnabled(marker)) {
            postLogMessage(LogLevel.info, marker, formatString, args);
        }
    } // info()

    public void info(String string, Throwable throwable) {
        if (isInfoEnabled()) {
            postLogMessage(LogLevel.info, string, throwable);
        }
    } // info()

    public void info(Throwable throwable, String string, Object... args) {
        if (isInfoEnabled()) {
            postLogMessage(LogLevel.info, string, throwable, args);
        }
    } // info()

    public boolean isInfoEnabled(Marker marker) {
        return _wrappedLogger.isInfoEnabled(marker);
    } // isInfoEnabled()

    public boolean isInfoEnabled() {
        return _wrappedLogger.isInfoEnabled();
    } // isInfoEnabled()

    /*----------------------------------------------------------------------------
        warn level logging statements
    -----------------------------------------------------------------------------*/

    public void warn(String string) {
        if (isWarnEnabled()) {
            postLogMessage(LogLevel.warn, null, string);
        }
    } // warn()

    public void warn(Marker marker, String string) {
        if (isWarnEnabled(marker)) {
            postLogMessage(LogLevel.warn, marker, string);
        }
    } // warn()

    public void warn(String formatString, Object... args) {
        if (isWarnEnabled()) {
            postLogMessage(LogLevel.warn, null, formatString, args);
        }
    } // warn()

    public void warn(Marker marker, String formatString, Object... args) {
        if (isWarnEnabled(marker)) {
            postLogMessage(LogLevel.warn, marker, formatString, args);
        }
    } // warn()

    public void warn(String string, Throwable throwable) {
        if (isWarnEnabled()) {
            postLogMessage(LogLevel.warn, string, throwable);
        }
    } // warn()

    public void warn(Throwable throwable, String string, Object... args) {
        if (isWarnEnabled()) {
            postLogMessage(LogLevel.warn, string, throwable, args);
        }
    } // warn()

    public boolean isWarnEnabled(Marker marker) {
        return _wrappedLogger.isWarnEnabled(marker);
    } // isWarnEnabled()

    public boolean isWarnEnabled() {
        return _wrappedLogger.isWarnEnabled();
    } // isWarnEnabled()

    /*----------------------------------------------------------------------------
        error level logging statements
    -----------------------------------------------------------------------------*/

    public void error(String string) {
        if (isErrorEnabled()) {
            postLogMessage(LogLevel.error, null, string);
        }
    } // error()

    public void error(Marker marker, String string) {
        if (isErrorEnabled(marker)) {
            postLogMessage(LogLevel.error, marker, string);
        }
    } // error()

    public void error(String formatString, Object... args) {
        if (isErrorEnabled()) {
            postLogMessage(LogLevel.error, null, formatString, args);
        }
    } // error()

    public void error(Marker marker, String formatString, Object... args) {
        if (isErrorEnabled(marker)) {
            postLogMessage(LogLevel.error, marker, formatString, args);
        }
    } // error()

    public void error(String string, Throwable throwable) {
        if (isErrorEnabled()) {
            postLogMessage(LogLevel.error, string, throwable);
        }
    } // error()

    public void error(Throwable throwable, String string, Object... args) {
        if (isErrorEnabled()) {
            postLogMessage(LogLevel.error, string, throwable, args);
        }
    } // error()

    public boolean isErrorEnabled(Marker marker) {
        return _wrappedLogger.isErrorEnabled(marker);
    } // isErrorEnabled()

    public boolean isErrorEnabled() {
        return _wrappedLogger.isErrorEnabled();
    } // isErrorEnabled()

    private String getMessage(StackTraceElement trace, String formatString, Object... args) {
        String message = formatString;

        try {
            if (null != args && args.length > 0) {
                if (null != formatString) {
                    message = String.format(formatString, args);
                } else {
                    StringBuilder sb = new StringBuilder();
                    for (Object arg : args) {
                        if (sb.length() > 0) {
                            sb.append(",");
                        }
                        sb.append(arg);
                    }
                    message = sb.toString();
                }
            }
        } catch (IllegalFormatException e) {
            List<Object> objList = new ArrayList<Object>();
            objList.add("error creating log message");
            objList.add(formatString);
            objList.addAll(Arrays.asList(args));
            error(null, objList.toArray());
        }

        if (null != trace && wantMethodAndLine()) {
            String className = trace.getClassName();
            message = String.format(
                "[%s.%s():%d] %s",
                className.substring(className.lastIndexOf('.') + 1),
                trace.getMethodName(),
                trace.getLineNumber(),
                message
            );
        }

        return message;
    } // getMessage()

    private void postLogMessage(LogLevel level,
                                Marker marker,
                                String message,
                                Object... args) {
        StackTraceElement trace = wantMethodAndLine()
                                  ? Thread.currentThread().getStackTrace()[3] : null;
        pushMessage(new LogMessage(level, marker, message, trace, args));
    } // postLogMessage()

    private void postLogMessage(LogLevel level,
                                String message,
                                Throwable throwable,
                                Object... args) {
        StackTraceElement trace = wantMethodAndLine()
                                  ? Thread.currentThread().getStackTrace()[3] : null;
        pushMessage(new LogMessage(level, message, trace, throwable, args));
    } // postLogMessage()

    private class LogMessage {

        // log level
        private LogLevel _logLevel;
        public LogLevel getLogLevel() { return _logLevel; }
        public void setLogLevel(LogLevel logLevel) { _logLevel = logLevel; }

        // Marker
        private Marker _marker;
        public Marker getMarker() { return _marker; }
        public void setMarker(Marker marker) { _marker = marker; }

        // message
        private String _message;
        public String getMessage() { return _message; }
        public void setMessage(String message) { _message = message; }

        // args
        private Object[] _args;
        public Object[] getArgs() { return _args; }
        public void setArgs(Object[] args) { _args = args; }

        // stack trace element
        private StackTraceElement _trace;
        public StackTraceElement getTrace() { return _trace; }
        public void setTrace(StackTraceElement trace) { _trace = trace; }

        // throwable
        private Throwable _throwable;
        public Throwable getThrowable() { return _throwable; }
        public void setThrowable(Throwable throwable) { _throwable = throwable; }

        public LogMessage(LogLevel level,
                          String message,
                          StackTraceElement trace,
                          Throwable throwable,
                          Object... args) {
            this(level, null, message, trace, args);
            _throwable = throwable;
        }

        public LogMessage(LogLevel level,
                          Marker marker,
                          String message,
                          StackTraceElement trace,
                          Object... args) {
            _logLevel = level;
            _marker = marker;
            _message = message;
            _trace = trace;
            _args = args;
        }

        public boolean hasMarker() {
            return null != _marker;
        } // hasMarker()

        public boolean hasArgs() {
            return null != _args && _args.length > 0;
        } // hasArgs()

        public boolean hasThrowable() {
            return null != _throwable;
        } // hasThrowable()

    }

} // class Logger
